/**
 * Aptana Studio
 * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
 * Please see the license.html included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.theme.internal;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import org.eclipse.core.internal.preferences.Base64;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.resource.DataFormatException;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.StringConverter;
import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.AnnotationPreference;
import org.eclipse.ui.texteditor.MarkerAnnotationPreferences;
import org.osgi.framework.Bundle;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.CollectionsUtil;
import com.aptana.core.util.EclipseUtil;
import com.aptana.core.util.IOUtil;
import com.aptana.core.util.StringUtil;
import com.aptana.scope.ScopeSelector;
import com.aptana.theme.IThemeManager;
import com.aptana.theme.Theme;
import com.aptana.theme.ThemePlugin;
import com.aptana.theme.ThemeRule;
import com.aptana.theme.internal.preferences.ThemerPreferenceInitializer;
import com.aptana.theme.preferences.IPreferenceConstants;
import com.aptana.ui.util.UIUtils;

@SuppressWarnings("restriction")
public class ThemeManager implements IThemeManager
{

	/**
	 * Node in preferences used to store themes under. Each theme is a key value pair under this node. The key is the
	 * theme name, value is XML format java Properties object.
	 */
	public static final String THEMES_NODE = "themes"; //$NON-NLS-1$
	// TODO Don't expose this node name. Fold saving/loading of themes into this impl

	private volatile Theme fCurrentTheme;
	private Set<String> fBuiltins;
	private Set<String> fThemeNames;

	private static ThemeManager fgInstance;

	/**
	 * The common prefixes of prefs related to annotations that we typically modify
	 */
	private static final String[] annotationKeyPrefixes = new String[] { "pydevOccurrenceIndication", //$NON-NLS-1$
			"searchResultIndication", //$NON-NLS-1$
			"xmlTagPairOccurrenceIndication", //$NON-NLS-1$
			"htmlTagPairOccurrenceIndication", //$NON-NLS-1$
			"rubyBlockPairOccurrenceIndication", //$NON-NLS-1$
	};

	private ThemeManager()
	{
		InstanceScope.INSTANCE.getNode("org.eclipse.ui.editors").addPreferenceChangeListener( //$NON-NLS-1$
				new IPreferenceChangeListener()
				{

					public void preferenceChange(PreferenceChangeEvent event)
					{
						// Listen to see if the user is modifying the annotations through Annotations pref page
						for (String prefix : annotationKeyPrefixes)
						{
							if (event.getKey().startsWith(prefix))
							{
								final String scopeSelector = "override." + prefix; //$NON-NLS-1$
								// If it's color and getting set to null, then it probably means that user
								// chose to restore defaults. Does that mean we should remove override?
								if (event.getNewValue() == null && event.getKey().endsWith("Color")) //$NON-NLS-1$
								{
									// Do we need to run this in a delayed job to avoid clashes when the other pref
									// changes come through at same time...?
									Job job = new UIJob("Restoring overrides of Annotation") //$NON-NLS-1$
									{
										@Override
										public IStatus runInUIThread(IProgressMonitor monitor)
										{
											ThemeRule rule = getCurrentTheme().getRuleForSelector(
													new ScopeSelector(scopeSelector));
											if (rule != null)
											{
												getCurrentTheme().remove(rule);
											}
											return Status.OK_STATUS;
										}
									};
									EclipseUtil.setSystemForJob(job);
									job.setPriority(Job.DECORATE);
									job.schedule();
								}
								else
								{
									if (!getCurrentTheme().hasEntry(scopeSelector))
									{
										// Store that the user has overridden this annotation in this theme
										int index = getCurrentTheme().getTokens().size();
										getCurrentTheme().addNewRule(index, "Annotation Override - " + prefix, //$NON-NLS-1$
												new ScopeSelector(scopeSelector), null);
									}

								}
								break;
							}
						}
					}
				});
	}

	public synchronized static ThemeManager instance()
	{
		if (fgInstance == null)
		{
			fgInstance = new ThemeManager();
		}
		return fgInstance;
	}

	private TextAttribute getTextAttribute(String name)
	{
		if (getCurrentTheme() != null)
		{
			return getCurrentTheme().getTextAttribute(name);
		}
		return new TextAttribute(ThemePlugin.getDefault().getColorManager().getColor(new RGB(255, 255, 255)));
	}

	/**
	 * Lazily init the current theme.
	 */
	public Theme getCurrentTheme()
	{
		if (fCurrentTheme == null)
		{
			synchronized (this)
			{
				String activeThemeName = Platform.getPreferencesService().getString(ThemePlugin.PLUGIN_ID,
						IPreferenceConstants.ACTIVE_THEME, ThemerPreferenceInitializer.DEFAULT_THEME, null);
				if (activeThemeName != null)
				{
					fCurrentTheme = getTheme(activeThemeName);
				}
				if (fCurrentTheme == null)
				{
					// if we can't find the default theme, just use the first one in the list
					if (!getThemeNames().isEmpty())
					{
						fCurrentTheme = getTheme(getThemeNames().iterator().next());
					}
				}
				if (fCurrentTheme != null)
				{
					setCurrentTheme(fCurrentTheme);
				}
			}
		}
		return fCurrentTheme;
	}

	/**
	 * Set the new theme to use, this involves setting prefs across a number of plugins.
	 */
	public void setCurrentTheme(Theme theme)
	{
		fCurrentTheme = theme;

		// Set the find in file search color
		setSearchResultColor(theme);

		// Set the color for the search result annotation, the pref key is "searchResultIndicationColor"
		setAnnotationColorsToMatchTheme(theme);

		// Also set the standard eclipse editor props, like fg, bg, selection fg, bg
		setAptanaEditorColorsToMatchTheme(theme);

		// Set the diff/compare colors based on theme
		setCompareColors("com.aptana.editor.common", true); //$NON-NLS-1$
		setCompareColors("org.eclipse.ui.editors", ThemePlugin.applyToAllEditors()); //$NON-NLS-1$

		// We notify in UI-thread because of APSTUD-7392
		// (in practice this almost always happens in the UI thread anyways, but it's
		// possible that at some circumstance this happens from a background thread).
		UIUtils.runInUIThread(new Runnable()
		{

			public void run()
			{
				notifyThemeChangeListeners(fCurrentTheme);
			}
		});

		forceFontsUpToDate();
	}

	// APSTUD-4152
	private void setCompareColors(String nodeName, boolean override)
	{
		IEclipsePreferences instancePrefs = InstanceScope.INSTANCE.getNode(nodeName);

		if (override)
		{
			RGB bg = getCurrentTheme().getBackground();
			RGB inverted = new RGB(255 - bg.red, 255 - bg.green, 255 - bg.blue);

			JFaceResources.getColorRegistry().put("INCOMING_COLOR", inverted); //$NON-NLS-1$
			JFaceResources.getColorRegistry().put("OUTGOING_COLOR", inverted); //$NON-NLS-1$
			instancePrefs.put("INCOMING_COLOR", StringConverter.asString(inverted)); //$NON-NLS-1$
			instancePrefs.put("OUTGOING_COLOR", StringConverter.asString(inverted)); //$NON-NLS-1$
		}
		else
		{
			// Revert to defaults if we have them
			IEclipsePreferences defPrefs = DefaultScope.INSTANCE.getNode(nodeName);
			String value = defPrefs.get("OUTGOING_COLOR", null); //$NON-NLS-1$
			if (value != null)
			{
				try
				{
					RGB rgb = StringConverter.asRGB(value);
					if (rgb != null)
					{
						JFaceResources.getColorRegistry().put("OUTGOING_COLOR", rgb); //$NON-NLS-1$
					}
				}
				catch (DataFormatException e)
				{
					// ignore
				}
			}
			value = defPrefs.get("INCOMING_COLOR", null); //$NON-NLS-1$
			if (value != null)
			{
				try
				{
					RGB rgb = StringConverter.asRGB(value);
					if (rgb != null)
					{
						JFaceResources.getColorRegistry().put("INCOMING_COLOR", rgb); //$NON-NLS-1$
					}
				}
				catch (DataFormatException e)
				{
					// ignore
				}
			}

			// Now remove the instance prefs
			instancePrefs.remove("INCOMING_COLOR"); //$NON-NLS-1$
			instancePrefs.remove("OUTGOING_COLOR"); //$NON-NLS-1$
		}

		try
		{
			instancePrefs.flush();
		}
		catch (BackingStoreException e)
		{
			IdeLog.logError(ThemePlugin.getDefault(), e);
		}
	}

	private void setSearchResultColor(Theme theme)
	{
		IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode("org.eclipse.search"); //$NON-NLS-1$
		prefs.put("org.eclipse.search.potentialMatch.fgColor", toString(theme.getSearchResultColor())); //$NON-NLS-1$
		try
		{
			prefs.flush();
		}
		catch (BackingStoreException e)
		{
			IdeLog.logError(ThemePlugin.getDefault(), e);
		}
	}

	private void forceFontsUpToDate()
	{
		final String[] fontIds = new String[] { IThemeManager.VIEW_FONT_NAME, JFaceResources.TEXT_FONT,
				"org.eclipse.ui.workbench.texteditor.blockSelectionModeFont" }; //$NON-NLS-1$
		UIUtils.getDisplay().asyncExec(new Runnable()
		{

			public void run()
			{
				for (String fontId : fontIds)
				{
					Font fFont = JFaceResources.getFontRegistry().get(fontId);
					// Only set new values if they're different from existing!
					Font existing = JFaceResources.getFont(fontId);
					String existingString = StringUtil.EMPTY;
					if (!existing.isDisposed())
					{
						existingString = PreferenceConverter.getStoredRepresentation(existing.getFontData());
					}
					String fdString = PreferenceConverter.getStoredRepresentation(fFont.getFontData());
					if (!existingString.equals(fdString))
					{
						// put in registry...
						JFaceResources.getFontRegistry().put(fontId, fFont.getFontData());
					}
				}
			}
		});
	}

	/**
	 * Set specific pref values that we use to listen for when the theme has changed across our plugins. This ignals to
	 * them the theme has been changed and they need to update their settings to match.
	 * 
	 * @param theme
	 */
	private void notifyThemeChangeListeners(Theme theme)
	{
		IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode(ThemePlugin.PLUGIN_ID);
		prefs.put(IPreferenceConstants.ACTIVE_THEME, theme.getName());
		prefs.putLong(THEME_CHANGED, System.currentTimeMillis());
		try
		{
			prefs.flush();
		}
		catch (BackingStoreException e)
		{
			IdeLog.logError(ThemePlugin.getDefault(), e);
		}
	}

	/**
	 * Set the FG, BG, selection and current line colors on our editors.
	 * 
	 * @param theme
	 */
	private void setAptanaEditorColorsToMatchTheme(Theme theme)
	{
		IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode("com.aptana.editor.common"); //$NON-NLS-1$
		prefs.putBoolean(AbstractTextEditor.PREFERENCE_COLOR_SELECTION_FOREGROUND_SYSTEM_DEFAULT, false);
		prefs.put(AbstractTextEditor.PREFERENCE_COLOR_SELECTION_FOREGROUND, toString(theme.getForeground()));

		prefs.putBoolean(AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT, false);
		prefs.put(AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND, toString(theme.getBackground()));

		prefs.putBoolean(AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT, false);
		prefs.put(AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND, toString(theme.getForeground()));

		prefs.put(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_CURRENT_LINE_COLOR,
				toString(theme.getLineHighlightAgainstBG()));
		try
		{
			prefs.flush();
		}
		catch (BackingStoreException e)
		{
			IdeLog.logError(ThemePlugin.getDefault(), e);
		}
	}

	private void setAnnotationColorsToMatchTheme(Theme theme)
	{
		IEclipsePreferences prefs = InstanceScope.INSTANCE.getNode("org.eclipse.ui.editors"); //$NON-NLS-1$
		if (!theme.hasEntry("override.searchResultIndication")) //$NON-NLS-1$
		{
			prefs.put("searchResultIndicationColor", toString(theme.getSearchResultColor())); //$NON-NLS-1$
		}
		// TODO Use markup.changed bg color for "decoration color" in Prefs>General>Appearance>Colors and Fonts

		// TODO Move this stuff over to theme change listeners in the XML/HTML/Ruby editor plugins?
		if (!theme.hasEntry("override.xmlTagPairOccurrenceIndication")) //$NON-NLS-1$
		{
			prefs.putBoolean("xmlTagPairOccurrenceIndicationHighlighting", false); //$NON-NLS-1$
			prefs.putBoolean("xmlTagPairOccurrenceIndication", true); //$NON-NLS-1$
			prefs.put("xmlTagPairOccurrenceIndicationColor", toString(theme.getOccurenceHighlightColor())); //$NON-NLS-1$
			prefs.put("xmlTagPairOccurrenceIndicationTextStyle", AnnotationPreference.STYLE_BOX); //$NON-NLS-1$
		}
		if (!theme.hasEntry("override.htmlTagPairOccurrenceIndication")) //$NON-NLS-1$
		{
			prefs.putBoolean("htmlTagPairOccurrenceIndicationHighlighting", false); //$NON-NLS-1$
			prefs.putBoolean("htmlTagPairOccurrenceIndication", true); //$NON-NLS-1$
			prefs.put("htmlTagPairOccurrenceIndicationColor", toString(theme.getOccurenceHighlightColor())); //$NON-NLS-1$
			prefs.put("htmlTagPairOccurrenceIndicationTextStyle", AnnotationPreference.STYLE_BOX); //$NON-NLS-1$
		}
		if (!theme.hasEntry("override.rubyBlockPairOccurrenceIndication")) //$NON-NLS-1$
		{
			prefs.putBoolean("rubyBlockPairOccurrenceIndicationHighlighting", false); //$NON-NLS-1$
			prefs.putBoolean("rubyBlockPairOccurrenceIndication", true); //$NON-NLS-1$
			prefs.put("rubyBlockPairOccurrenceIndicationColor", toString(theme.getOccurenceHighlightColor())); //$NON-NLS-1$
			prefs.put("rubyBlockPairOccurrenceIndicationTextStyle", AnnotationPreference.STYLE_BOX); //$NON-NLS-1$
		}
		// PyDev Occurrences (com.python.pydev.occurrences)
		// Override them if pydev is set to use our themes
		if (Platform.getPreferencesService().getBoolean("org.python.pydev.red_core", "PYDEV_USE_APTANA_THEMES", true, //$NON-NLS-1$ //$NON-NLS-2$
				null))
		{
			if (!theme.hasEntry("override.pydevOccurrenceIndication")) //$NON-NLS-1$
			{
				MarkerAnnotationPreferences preferences = new MarkerAnnotationPreferences();
				AnnotationPreference pydevOccurPref = null;
				for (Object obj : preferences.getAnnotationPreferences())
				{
					AnnotationPreference pref = (AnnotationPreference) obj;
					Object type = pref.getAnnotationType();
					if ("com.python.pydev.occurrences".equals(type)) //$NON-NLS-1$
					{
						pydevOccurPref = pref;
					}
				}
				if (pydevOccurPref != null)
				{
					if (pydevOccurPref.getTextStylePreferenceKey() != null)
					{
						// Now that pydev supports text style, use the box style and don't highlight.
						prefs.putBoolean("pydevOccurrenceHighlighting", false); //$NON-NLS-1$
						prefs.putBoolean("pydevOccurrenceIndication", true); //$NON-NLS-1$
						prefs.put("pydevOccurrenceIndicationColor", toString(theme.getOccurenceHighlightColor())); //$NON-NLS-1$
						prefs.put("pydevOccurrenceIndicationTextStyle", AnnotationPreference.STYLE_BOX); //$NON-NLS-1$
					}
					else
					{
						// Must use highlighting, since we're against older pydev that had no text style
						prefs.putBoolean("pydevOccurrenceHighlighting", true); //$NON-NLS-1$
						prefs.putBoolean("pydevOccurrenceIndication", true); //$NON-NLS-1$
						prefs.put("pydevOccurrenceIndicationColor", toString(theme.getSearchResultColor())); //$NON-NLS-1$
					}
				}
			}
		}

		try
		{
			prefs.flush();
		}
		catch (BackingStoreException e)
		{
			IdeLog.logError(ThemePlugin.getDefault(), e);
		}
	}

	private static String toString(RGB selection)
	{
		return StringConverter.asString(selection);
	}

	/**
	 * Attempts to find the theme with a given name, first from prefs, then from pre-packaged builtins. Will return null
	 * if no match is found.
	 */
	public Theme getTheme(String name)
	{
		// Try to see if we have a copy in prefs as a user theme
		Theme loaded = null;
		try
		{
			loaded = loadUserTheme(name);
		}
		catch (Exception e)
		{
			IdeLog.logError(ThemePlugin.getDefault(),
					MessageFormat.format("Failed to load theme {0} from preferences.", name), e); //$NON-NLS-1$
		}
		if (loaded != null)
		{
			return loaded;
		}
		// Ok, no user theme by that name, load up the builtins. Loading them once should save a copy to prefs (user)
		// for future...
		try
		{
			return loadBuiltinTheme(name);
		}
		catch (Exception e)
		{
			IdeLog.logError(ThemePlugin.getDefault(),
					MessageFormat.format("Failed to load theme {0} from builtins.", name), e); //$NON-NLS-1$
		}
		return null;
	}

	/**
	 * laziliy init the set of theme names.
	 */
	public synchronized Set<String> getThemeNames()
	{
		if (fThemeNames == null)
		{
			fThemeNames = new HashSet<String>();
			// Add names of themes from builtins...
			fThemeNames.addAll(getBuiltinThemeNames());

			// Look in prefs to see what user themes are stored there, garb their names
			IScopeContext[] scopes = new IScopeContext[] { InstanceScope.INSTANCE, DefaultScope.INSTANCE };
			for (IScopeContext scope : scopes)
			{
				IEclipsePreferences prefs = scope.getNode(ThemePlugin.PLUGIN_ID);
				Preferences preferences = prefs.node(ThemeManager.THEMES_NODE);
				try
				{
					String[] themeNames = preferences.keys();
					fThemeNames.addAll(Arrays.asList(themeNames));
				}
				catch (BackingStoreException e)
				{
					IdeLog.logError(ThemePlugin.getDefault(), e);
				}
			}
		}
		return fThemeNames;
	}

	private Theme loadUserTheme(String themeName)
	{
		InputStream byteStream = null;
		try
		{
			byte[] array = Platform.getPreferencesService().getByteArray(ThemePlugin.PLUGIN_ID,
					THEMES_NODE + "/" + themeName, null, null); //$NON-NLS-1$
			if (array == null)
			{
				return null;
			}
			byteStream = new ByteArrayInputStream(array);
			Properties props = new OrderedProperties();
			props.load(byteStream);
			// if it looks like the byte array was not Base64 decoded, try decoding and then running it through
			if (!props.containsKey(Theme.THEME_NAME_PROP_KEY)) // anything else we can check for this?
			{
				IdeLog.logWarning(
						ThemePlugin.getDefault(),
						MessageFormat
								.format("User theme {0} de-serialized, but was left Base64 encoded. Manually decoding and trying to load.", //$NON-NLS-1$
										themeName));
				byteStream = new ByteArrayInputStream(Base64.decode(array));
				props = new OrderedProperties();
				props.load(byteStream);
			}
			return new Theme(ThemePlugin.getDefault().getColorManager(), props);
		}
		catch (IllegalArgumentException iae)
		{
			// Fallback to load theme that was saved in prefs as XML string
			String xml = Platform.getPreferencesService().getString(ThemePlugin.PLUGIN_ID,
					THEMES_NODE + "/" + themeName, null, null); //$NON-NLS-1$
			if (xml != null)
			{
				InputStream stream = null;
				try
				{
					stream = new ByteArrayInputStream(xml.getBytes(IOUtil.UTF_8));
					Properties props = new OrderedProperties();
					props.loadFromXML(stream);
					// Now store it as byte array explicitly so we don't run into this!
					Theme theme = new Theme(ThemePlugin.getDefault().getColorManager(), props);
					theme.save();
					return theme;
				}
				catch (Exception e)
				{
					IdeLog.logError(ThemePlugin.getDefault(), e);
				}
				finally
				{
					if (stream != null)
					{
						try
						{
							stream.close();
						}
						catch (IOException e)
						{
							// ignore
						}
					}
				}
			}
		}
		catch (IOException e)
		{
			IdeLog.logError(ThemePlugin.getDefault(), e);
		}
		finally
		{
			if (byteStream != null)
			{
				try
				{
					byteStream.close();
				}
				catch (IOException e)
				{
					// ignore
				}
			}
		}
		return null;
	}

	private OrderedProperties getBuiltinThemeProperties(String themeName)
	{
		Collection<URL> urls = getBuiltinThemeURLs();
		if (CollectionsUtil.isEmpty(urls))
		{
			return null;
		}

		for (URL url : urls)
		{
			try
			{
				// Try forcing the file to be extracted out from zip before we try to read it
				InputStream stream = FileLocator.toFileURL(url).openStream();
				try
				{
					OrderedProperties props = new OrderedProperties();
					props.load(stream);
					String loadedName = props.getProperty(Theme.THEME_NAME_PROP_KEY);
					if (!themeName.equals(loadedName))
					{
						continue;
					}

					String multipleThemeExtends = props.getProperty(Theme.THEME_EXTENDS_PROP_KEY);
					// If we extend one or more other themes, recursively load their properties...
					if (multipleThemeExtends != null)
					{
						OrderedProperties newProperties = new OrderedProperties();
						String[] pieces = multipleThemeExtends.split(","); //$NON-NLS-1$
						for (String themeExtends : pieces)
						{
							Properties extended = getBuiltinThemeProperties(themeExtends);
							if (extended == null)
							{
								throw new IllegalStateException(MessageFormat.format(
										Messages.ThemeManager_ERR_NoThemeFound, themeExtends, loadedName));
							}
							newProperties.putAll(extended);
						}
						newProperties.putAll(props);
						// We don't want the final extends props in the properties.
						newProperties.remove(Theme.THEME_EXTENDS_PROP_KEY);
						// Sanity check
						Assert.isTrue(newProperties.get(Theme.THEME_NAME_PROP_KEY).equals(themeName));
						return newProperties;
					}
					return props;
				}
				finally
				{
					try
					{
						stream.close();
					}
					catch (IOException e)
					{
						// ignore
					}
				}
			}
			catch (Exception e)
			{
				IdeLog.logError(ThemePlugin.getDefault(), url.toString(), e);
			}
		}
		return null;
	}

	private synchronized Set<String> getBuiltinThemeNames()
	{
		if (fBuiltins == null)
		{
			fBuiltins = new HashSet<String>();
			Collection<URL> urls = getBuiltinThemeURLs();
			if (urls == null || urls.isEmpty())
			{
				return fBuiltins;
			}

			for (URL url : urls)
			{
				InputStream stream = null;
				try
				{
					// Try forcing the file to be extracted out from zip before we try to read it
					stream = FileLocator.toFileURL(url).openStream();
					OrderedProperties props = new OrderedProperties();
					props.load(stream);
					String loadedName = props.getProperty(Theme.THEME_NAME_PROP_KEY);
					// Don't include the abstract themes in the list, they're meant just for extending
					if (loadedName != null && !loadedName.startsWith("abstract_theme")) //$NON-NLS-1$
					{
						fBuiltins.add(loadedName);
					}
				}
				catch (Exception e)
				{
					IdeLog.logError(ThemePlugin.getDefault(), e);
				}
				finally
				{
					try
					{
						if (stream != null)
						{
							stream.close();
						}
					}
					catch (IOException e)
					{
						// ignore
					}
				}
			}
		}
		return fBuiltins;
	}

	private Collection<URL> getBuiltinThemeURLs()
	{
		ThemePlugin themePlugin = ThemePlugin.getDefault();
		if (themePlugin == null)
		{
			return Collections.emptyList();
		}
		Bundle bundle = themePlugin.getBundle();
		if (bundle == null)
		{
			return Collections.emptyList();
		}
		ArrayList<URL> collection = new ArrayList<URL>();
		Enumeration<URL> enumeration = bundle.findEntries("themes", "*.properties", false); //$NON-NLS-1$ //$NON-NLS-2$
		while (enumeration.hasMoreElements())
		{
			collection.add(enumeration.nextElement());
		}
		collection.trimToSize();
		return collection;
	}

	public Theme loadBuiltinTheme(String themeName)
	{
		OrderedProperties properties = getBuiltinThemeProperties(themeName);
		if (properties == null)
		{
			return null;
		}
		return loadBuiltinTheme(properties);
	}

	private Theme loadBuiltinTheme(Properties props)
	{
		try
		{
			return new Theme(ThemePlugin.getDefault().getColorManager(), props);
		}
		catch (Exception e)
		{
			IdeLog.logError(ThemePlugin.getDefault(), e);
		}
		return null;
	}

	public IToken getToken(String scope)
	{
		return new Token(getTextAttribute(scope));
	}

	public void addTheme(Theme newTheme)
	{
		newTheme.save();
		getThemeNames().add(newTheme.getName());
	}

	public void removeTheme(Theme theme)
	{
		Theme activeTheme = getCurrentTheme();
		getThemeNames().remove(theme.getName());
		// change active theme if we just removed it
		if (activeTheme.getName().equals(theme.getName()))
		{
			// load first theme from list of names
			setCurrentTheme(getTheme(getThemeNames().iterator().next()));
		}
	}

	public boolean isBuiltinTheme(String themeName)
	{
		return getBuiltinThemeNames().contains(themeName);
	}

	public IStatus validateThemeName(String name)
	{
		if (StringUtil.isEmpty(name))
		{
			return new Status(IStatus.ERROR, ThemePlugin.PLUGIN_ID, Messages.ThemeManager_NameNonEmptyMsg);
		}
		if (getThemeNames().contains(name.trim()))
		{
			return new Status(IStatus.ERROR, ThemePlugin.PLUGIN_ID, Messages.ThemeManager_NameAlreadyExistsMsg);
		}
		return Status.OK_STATUS;
	}
}
